#!/usr/bin/python

# Start/stop Web2py Daemon
#
# chkconfig: - 90 10
# description: Web2py Daemon
# pidfile: /var/run/web2pyd.pid

#--- configeration ---
serverName = 'web2pyd'
PYTHON = '/usr/bin/python'
WORKDIR = '/var/web2py/' # change this if necessary
DAEMON = 'web2py.py'
#ADMINPASS = "\<recycle\>"
ADMINPASS = 'admin'
LISTEN = '0.0.0.0'
PORT = 8001
SETTINGS = '' # custom options configfile
schedulers = [] # list of application which needs scheduler

USER = 'root' # run as, name or uid
GROUP = 'root' # run as, name or gid

import os
import pwd
import signal
import sys
import syslog
import time

# Server files:
pidFolder = '/var/run/'
serverPidFile = 'web2pyd.pid'
schedulerPidFile = 'web2py_scheduler.pid'
serverConsoleLogFile = os.path.join(WORKDIR,'output.log')
schedulerConsoleLogFile = os.path.join(WORKDIR,'scheduler_output.log')


def checkIdentity():
    if os.getuid() != 0:
        raise Exception("This script should run with the root user.")

def checkDirectory(name):
    if not os.path.isdir(name):
        raise Exception("The directory \"%s\" doesn't exist." % name)


def checkFile(name):
    if not os.path.isfile(name):
        raise Exception("The file \"%s\" doesn't exist." % name)


def checkInstallation():
    checkDirectory(WORKDIR)
    checkFile(os.path.join(WORKDIR,DAEMON))

def _run_as(user,group):
    if group not in ['root',0]:
        try:
            gid = int(group)
        except:
            gid = pwd.getpwnam(group).pw_gid
        os.setegid(gid)
            
    if user not in ['root',0]:
        try:
            uid = int(user)
        except:
            uid = pwd.getpwnam(user).pw_uid
        os.seteuid(uid)
    return

def _loadPid(filename):
    filepath = os.path.join(pidFolder,filename)
    if not os.path.exists(filepath):
        return None
    with open(filepath, "r") as pidFd:
        return int(pidFd.read())

def loadServerPid():
    return _loadPid(serverPidFile)

def loadSchedulerPid(app):
    return _loadPid(schedulerPidFile+'.'+app)

def _savePid(pid,filename):
    filepath = os.path.join(pidFolder,filename)
    with open(filepath, "w") as pidFd:
        pidFd.write(str(pid) + "\n")

def saveServerPid(pid):
    return _savePid(pid,serverPidFile)

def saveSchedulerPid(app,pid):
    return _savePid(pid,schedulerPidFile+'.'+app)

def removeServerPid():
    filepath = os.path.join(pidFolder,serverPidFile)
    if os.path.exists(filepath):
        os.remove(filepath)

def removeSchedulerPid(app):
    filepath = os.path.join(pidFolder,schedulerPidFile)+'.'+app
    if os.path.exists(filepath):
        os.remove(filepath)

def startServer():
    # Get the PID:
    serverPid = loadServerPid()

    # If the PID already exists then we need to check if the
    # process is running and tell the user that the service needs
    # to be restarted:
    if serverPid:
        if not os.path.exists("/proc/%d" % serverPid):
            raise Exception(
                "The server PID file \"%s\" contains %d but "
                "that process doesn't exist. This means that "
                "the server crashed or was killed. You need "
                "to explicitly run 'service web2pyd stop' "
                "and then 'service web2pyd start' to "
                "enable it again." %
                (serverPidFile, serverPid))
        else:
            syslog.syslog(syslog.LOG_WARNING,
                "The server PID file \"%s\" exists and the "
                "process %d is running." %
                (serverPidFile, serverPid))
            return

    # We start with an empty list of arguments:
    serverArgs = [PYTHON]

    # Add arguments for the python:
    serverArgs.extend([
                       DAEMON,
                       '--nogui',
                       '-a',ADMINPASS,
                       ])
    if LISTEN:
        serverArgs.extend(['-i',LISTEN])
    if PORT:
        serverArgs.extend(['-p',str(PORT)])
    
    # add customized settings
    if SETTINGS:
        serverArgs.extend(['-L',SETTINGS])
    # Fork a new process:
    serverPid = os.fork()

    # If this is the parent process then the last thing we have to do is
    # saving the child process PID to the file:
    if serverPid != 0:
        syslog.syslog(syslog.LOG_INFO, "Started server process %d." % serverPid)
        saveServerPid(serverPid)
        return

    # Then close standard input and some other security measures:
    _run_as(USER,GROUP)
    os.close(0)
    os.setsid()
    os.chdir(WORKDIR)

    # Then open the console log and redirect standard output and errors to it:
    serverConsoleFd = os.open(serverConsoleLogFile, os.O_CREAT | os.O_WRONLY | os.O_APPEND, 0660)
    os.dup2(serverConsoleFd, 1)
    os.dup2(serverConsoleFd, 2)
    # not close this file ,so we can get output in the file.
    #os.close(serverConsoleFd)

    # Finally execute:
    os.execvp(PYTHON, serverArgs)

def startScheduler(app):
    # Get the PID:
    schedulerPid = loadSchedulerPid(app)

    # If the PID already exists then we need to check if the
    # process is running and tell the user that the service needs
    # to be restarted:
    if schedulerPid:
        if not os.path.exists("/proc/%d" % schedulerPid):
            raise Exception(
                "The scheduler PID file \"%s\" contains %d but "
                "that process doesn't exist. This means that "
                "the scheduler crashed or was killed. You need "
                "to explicitly run 'service web2pyd stop' "
                "and then 'service web2pyd start' to "
                "enable it again." %
                (schedulerPidFile, schedulerPid))
        else:
            syslog.syslog(syslog.LOG_WARNING,
                "The scheduler PID file \"%s\" exists and the "
                "process %d is running." %
                (schedulerPidFile, schedulerPid))
            return

    # We start with an empty list of arguments:
    serverArgs = [PYTHON]

    # Add arguments for the python:
    serverArgs.extend([
                       DAEMON,
                       '-K',
                       app
                       ])
    # Fork a new process:
    schedulerPid = os.fork()

    # If this is the parent process then the last thing we have to do is
    # saving the child process PID to the file:
    if schedulerPid != 0:
        syslog.syslog(syslog.LOG_INFO, "Started scheduler process %d." % schedulerPid)
        saveSchedulerPid(app,schedulerPid)
        return

    # Then close standard input and some other security measures:
    _run_as(USER,GROUP)
    os.close(0)
    os.setsid()
    os.chdir(WORKDIR)

    # Then open the console log and redirect standard output and errors to it:
    schedulerConsoleFd = os.open(schedulerConsoleLogFile, os.O_CREAT | os.O_WRONLY | os.O_APPEND, 0660)
    os.dup2(schedulerConsoleFd, 1)
    os.dup2(schedulerConsoleFd, 2)
    # not close this file ,so we can get output in the file.
    #os.close(schedulerConsoleFd)

    # Finally execute:
    os.execvp(PYTHON, serverArgs)


def stopServer():
    # Load the PID:
    serverPid = loadServerPid()
    if not serverPid:
        syslog.syslog(syslog.LOG_INFO, "The server PID file \"%s\" doesn't exist." % serverPidFile)
        return

    # First check that the process exists:
    if not os.path.exists("/proc/%d" % serverPid):
        syslog.syslog(syslog.LOG_WARNING, "The server PID file \"%s\" contains %d, but that process doesn't exist, will just remove the file." % (serverPidFile, serverPid))
        removeServerPid()
        return

    # Get the time to wait for the server to stop from the configuration:
    stopTime = 5
    stopInterval = 1

    # Kill the process softly and wait for it to dissapear or for the timeout
    # to expire:
    os.kill(serverPid, signal.SIGTERM)
    initialTime = time.time()
    timeElapsed = 0
    while os.path.exists("/proc/%d" % serverPid):
        syslog.syslog(syslog.LOG_INFO, "Waiting up to %d seconds for server process %d to finish." % ((stopTime - timeElapsed), serverPid))
        timeElapsed = time.time() - initialTime
        if timeElapsed > stopTime:
            break
        time.sleep(stopInterval)

    # If the process didn't dissapear after the allowed time then we forcibly
    # kill it:
    if os.path.exists("/proc/%d" % serverPid):
        syslog.syslog(syslog.LOG_WARNING, "The server process %d didn't finish after waiting %d seconds, killing it." % (serverPid, timeElapsed))
        os.kill(serverPid, signal.SIGKILL)
        syslog.syslog(syslog.LOG_WARNING, "Killed server process %d." % serverPid)
    else:
        syslog.syslog(syslog.LOG_INFO, "Stopped server process %d." % serverPid)

    # Remove the PID file:
    removeServerPid()

def stopScheduler(app):
    # Load the PID:
    schedulerPid = loadSchedulerPid(app)
    if not schedulerPid:
        syslog.syslog(syslog.LOG_INFO, "The scheduler PID file \"%s\" doesn't exist." % schedulerPidFile)
        return

    # First check that the process exists:
    if not os.path.exists("/proc/%d" % schedulerPid):
        syslog.syslog(syslog.LOG_WARNING, "The scheduler PID file \"%s\" contains %d, but that process doesn't exist, will just remove the file." % (schedulerPidFile, schedulerPid))
        removeSchedulerPid(app)
        return

    # Get the time to wait for the scheduler to stop from the configuration:
    stopTime = 5
    stopInterval = 1

    # Kill the process softly and wait for it to dissapear or for the timeout
    # to expire:
    os.kill(schedulerPid, signal.SIGTERM)
    initialTime = time.time()
    timeElapsed = 0
    while os.path.exists("/proc/%d" % schedulerPid):
        syslog.syslog(syslog.LOG_INFO, "Waiting up to %d seconds for scheduler process %d to finish." % ((stopTime - timeElapsed), schedulerPid))
        timeElapsed = time.time() - initialTime
        if timeElapsed > stopTime:
            break
        time.sleep(stopInterval)

    # If the process didn't dissapear after the allowed time then we forcibly
    # kill it:
    if os.path.exists("/proc/%d" % schedulerPid):
        syslog.syslog(syslog.LOG_WARNING, "The scheduler process %d didn't finish after waiting %d seconds, killing it." % (schedulerPid, timeElapsed))
        os.kill(schedulerPid, signal.SIGKILL)
        syslog.syslog(syslog.LOG_WARNING, "Killed scheduler process %d." % schedulerPid)
    else:
        syslog.syslog(syslog.LOG_INFO, "Stopped scheduler:%s process %d." % (app,schedulerPid))

    # Remove the PID file:
    removeSchedulerPid(app)

def checkServer():
    # First check that the server PID file exists, if it doesn't
    # then we assume that the server is not running:
    serverPid = loadServerPid()
    if not serverPid:
        print("The server is not running.")
        sys.exit(3)

    # Now check that the process exists:
    if not os.path.exists("/proc/%d" % serverPid):
        print("The server process %d is not running." % serverPid)
        sys.exit(1)

    # XXX: Here we could check deeper the status of the server sending a
    # request to the health status servlet.
    print("The server process %d is running." % serverPid)
    
    ret = 0
    from glob import glob
    for filepath in glob(os.path.join(pidFolder,schedulerPidFile)+'.*'):
        app = filepath.replace(os.path.join(pidFolder,schedulerPidFile)+'.','')
        schedulerPid = loadSchedulerPid(app)
        
        # Now check that the process exists:
        if not os.path.exists("/proc/%d" % schedulerPid):
            print("The scheduler:%s process %d is not running." % (app,schedulerPid))
            ret = 1
            continue
        # XXX: Here we could check deeper the status of the server sending a
        # request to the health status servlet.
        print("The scheduler:%s process %d is running." % (app,schedulerPid))
    
    sys.exit(ret)


def showUsage():
    print("Usage: service web2pyd {start|stop|restart|status}")


def prettyAction(label, action, *args, **vars):
    # Determine the colors to use according to the type of terminal:
    colorNormal = ""
    colorSuccess = ""
    colorFailure = ""
    moveColumn = ""
    if os.getenv("TERM") in ["linux", "xterm"]:
        colorNormal = "\033[0;39m"
        colorSuccess = "\033[0;32m"
        colorFailure  = "\033[0;31m"
        moveColumn = "\033[60G"

    # Inform that we are doing the job:
    sys.stdout.write(label)
    sys.stdout.flush()

    # Do the real action:
    try:
        action(*args,**vars)
        sys.stdout.write(moveColumn + " [  " + colorSuccess + "OK" + colorNormal + "  ]\n")
    except Exception as exception:
        sys.stdout.write(moveColumn + " [" + colorFailure + "FAILED" + colorNormal + "]\n")
        raise


def performAction(action):
    # The status action is a bit special, as it has some very
    # specific requirements regarding the exit codes, so they are
    # managed inside the function:
    if action == "status":
        try:
            checkServer()
        except SystemExit:
            raise
        except Exception as exception:
            print(str(exception))
            sys.exit(4)

    # The rest of the actions all work in the same way, if
    # everything goes well finish with exit code zero, otherwise
    # finish with non zero exit code:
    try:
        if action == "start":
            prettyAction("Starting %s:"%serverName, startServer)
            for app in schedulers:
                prettyAction('Starting scheduler for %s:'%app,startScheduler,app)
        elif action == "stop":
            # get schedulers
            from glob import glob
            for filepath in glob(os.path.join(pidFolder,schedulerPidFile)+'.*'):
                app = filepath.replace(os.path.join(pidFolder,schedulerPidFile)+'.','')
                prettyAction("Stopping scheduler for %s:"%app, stopScheduler,app)
            prettyAction("Stopping %s:"%serverName, stopServer)
        elif action == "restart":
            # stop
            # get schedulers
            from glob import glob
            for filepath in glob(os.path.join(pidFolder,schedulerPidFile)+'.*'):
                app = filepath.replace(os.path.join(pidFolder,schedulerPidFile)+'.','')
                prettyAction("Stopping scheduler for %s:"%app, stopScheduler,app)
            prettyAction("Stopping %s:"%serverName, stopServer)
            # start
            prettyAction("Starting %s:"%serverName, startServer)
            for app in schedulers:
                prettyAction('Starting scheduler for %s:'%app,startScheduler,app)
    except Exception as exception:
        syslog.syslog(syslog.LOG_ERR, str(exception))
        sys.exit(1)
    else:
        sys.exit(0)


def main():
    # Check the arguments:
    args = sys.argv[1:]
    if len(args) != 1:
        showUsage()
        sys.exit(1)
    action = args[0].lower()
    if not action in ["start", "stop", "restart", "status"]:
        showUsage()
        sys.exit(1)

    # Run the action with syslog open and remember to close it
    # regardless of what happens in the middle:
    syslog.openlog(serverName, syslog.LOG_PID)
    try:
        # perform checks:
        checkIdentity()
        checkInstallation()
        performAction(action)
    except Exception,e:
        print e
        sys.exit(1)
    finally:
        syslog.closelog()


if __name__ == "__main__":
    main()